// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'ast.dart';
import 'functional.dart';
import 'generator.dart';
import 'generator_tools.dart';
import 'pigeon_lib.dart' show Error;

/// General comment opening token.
const String _commentPrefix = '//';

const String _voidType = 'void';

/// Documentation comment spec.
const DocumentCommentSpecification _docCommentSpec =
    DocumentCommentSpecification(_commentPrefix);

/// The default serializer for Flutter.
const String _defaultCodecSerializer = 'flutter::StandardCodecSerializer';

/// Options that control how C++ code will be generated.
class CppOptions {
  /// Creates a [CppOptions] object
  const CppOptions({
    this.headerIncludePath,
    this.namespace,
    this.copyrightHeader,
    this.headerOutPath,
  });

  /// The path to the header that will get placed in the source filed (example:
  /// "foo.h").
  final String? headerIncludePath;

  /// The namespace where the generated class will live.
  final String? namespace;

  /// A copyright header that will get prepended to generated code.
  final Iterable<String>? copyrightHeader;

  /// The path to the output header file location.
  final String? headerOutPath;

  /// Creates a [CppOptions] from a Map representation where:
  /// `x = CppOptions.fromMap(x.toMap())`.
  static CppOptions fromMap(Map<String, Object> map) {
    return CppOptions(
      headerIncludePath: map['header'] as String?,
      namespace: map['namespace'] as String?,
      copyrightHeader: map['copyrightHeader'] as Iterable<String>?,
      headerOutPath: map['cppHeaderOut'] as String?,
    );
  }

  /// Converts a [CppOptions] to a Map representation where:
  /// `x = CppOptions.fromMap(x.toMap())`.
  Map<String, Object> toMap() {
    final Map<String, Object> result = <String, Object>{
      if (headerIncludePath != null) 'header': headerIncludePath!,
      if (namespace != null) 'namespace': namespace!,
      if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
    };
    return result;
  }

  /// Overrides any non-null parameters from [options] into this to make a new
  /// [CppOptions].
  CppOptions merge(CppOptions options) {
    return CppOptions.fromMap(mergeMaps(toMap(), options.toMap()));
  }
}

/// Class that manages all Cpp code generation.
class CppGenerator extends Generator<OutputFileOptions<CppOptions>> {
  /// Constructor.
  const CppGenerator();

  /// Generates C++ file of type specified in [generatorOptions]
  @override
  void generate(
    OutputFileOptions<CppOptions> generatorOptions,
    Root root,
    StringSink sink, {
    required String dartPackageName,
  }) {
    assert(generatorOptions.fileType == FileType.header ||
        generatorOptions.fileType == FileType.source);
    if (generatorOptions.fileType == FileType.header) {
      const CppHeaderGenerator().generate(
        generatorOptions.languageOptions,
        root,
        sink,
        dartPackageName: dartPackageName,
      );
    } else if (generatorOptions.fileType == FileType.source) {
      const CppSourceGenerator().generate(
        generatorOptions.languageOptions,
        root,
        sink,
        dartPackageName: dartPackageName,
      );
    }
  }
}

/// Writes C++ header (.h) file to sink.
class CppHeaderGenerator extends StructuredGenerator<CppOptions> {
  /// Constructor.
  const CppHeaderGenerator();

  @override
  void writeFilePrologue(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    if (generatorOptions.copyrightHeader != null) {
      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
    }
    indent.writeln('$_commentPrefix ${getGeneratedCodeWarning()}');
    indent.writeln('$_commentPrefix $seeAlsoWarning');
    indent.newln();
  }

  @override
  void writeFileImports(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    final String guardName = _getGuardName(generatorOptions.headerIncludePath);
    indent.writeln('#ifndef $guardName');
    indent.writeln('#define $guardName');

    _writeSystemHeaderIncludeBlock(indent, <String>[
      'flutter/basic_message_channel.h',
      'flutter/binary_messenger.h',
      'flutter/encodable_value.h',
      'flutter/standard_message_codec.h',
    ]);
    indent.newln();
    _writeSystemHeaderIncludeBlock(indent, <String>[
      'map',
      'string',
      'optional',
    ]);
    indent.newln();
    if (generatorOptions.namespace != null) {
      indent.writeln('namespace ${generatorOptions.namespace} {');
    }
    indent.newln();
    if (generatorOptions.namespace?.endsWith('_pigeontest') ?? false) {
      final String testFixtureClass =
          '${_pascalCaseFromSnakeCase(generatorOptions.namespace!.replaceAll('_pigeontest', ''))}Test';
      indent.writeln('class $testFixtureClass;');
    }
    indent.newln();
    indent.writeln('$_commentPrefix Generated class from Pigeon.');
  }

  @override
  void writeEnum(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Enum anEnum, {
    required String dartPackageName,
  }) {
    indent.newln();
    addDocumentationComments(
        indent, anEnum.documentationComments, _docCommentSpec);
    indent.write('enum class ${anEnum.name} ');
    indent.addScoped('{', '};', () {
      enumerate(anEnum.members, (int index, final EnumMember member) {
        addDocumentationComments(
            indent, member.documentationComments, _docCommentSpec);
        indent.writeln(
            '${member.name} = $index${index == anEnum.members.length - 1 ? '' : ','}');
      });
    });
  }

  @override
  void writeGeneralUtilities(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    _writeErrorOr(indent, friends: root.apis.map((Api api) => api.name));
  }

  @override
  void writeDataClass(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Class klass, {
    required String dartPackageName,
  }) {
    // When generating for a Pigeon unit test, add a test fixture friend class to
    // allow unit testing private methods, since testing serialization via public
    // methods is essentially an end-to-end test.
    String? testFixtureClass;
    if (generatorOptions.namespace?.endsWith('_pigeontest') ?? false) {
      testFixtureClass =
          '${_pascalCaseFromSnakeCase(generatorOptions.namespace!.replaceAll('_pigeontest', ''))}Test';
    }
    indent.newln();

    const List<String> generatedMessages = <String>[
      ' Generated class from Pigeon that represents data sent in messages.'
    ];

    addDocumentationComments(
        indent, klass.documentationComments, _docCommentSpec,
        generatorComments: generatedMessages);

    final Iterable<NamedType> orderedFields =
        getFieldsInSerializationOrder(klass);

    indent.write('class ${klass.name} ');
    indent.addScoped('{', '};', () {
      _writeAccessBlock(indent, _ClassAccess.public, () {
        final Iterable<NamedType> requiredFields =
            orderedFields.where((NamedType type) => !type.type.isNullable);
        // Minimal constructor, if needed.
        if (requiredFields.length != orderedFields.length) {
          _writeClassConstructor(root, indent, klass, requiredFields,
              'Constructs an object setting all non-nullable fields.');
        }
        // All-field constructor.
        _writeClassConstructor(root, indent, klass, orderedFields,
            'Constructs an object setting all fields.');

        for (final NamedType field in orderedFields) {
          addDocumentationComments(
              indent, field.documentationComments, _docCommentSpec);
          final HostDatatype baseDatatype = getFieldHostDatatype(
              field, root.classes, root.enums, _baseCppTypeForBuiltinDartType);
          // Declare a getter and setter.
          _writeFunctionDeclaration(indent, _makeGetterName(field),
              returnType: _getterReturnType(baseDatatype), isConst: true);
          final String setterName = _makeSetterName(field);
          _writeFunctionDeclaration(indent, setterName,
              returnType: _voidType,
              parameters: <String>[
                '${_unownedArgumentType(baseDatatype)} value_arg'
              ]);
          if (field.type.isNullable) {
            // Add a second setter that takes the non-nullable version of the
            // argument for convenience, since setting literal values with the
            // pointer version is non-trivial.
            final HostDatatype nonNullType = _nonNullableType(baseDatatype);
            _writeFunctionDeclaration(indent, setterName,
                returnType: _voidType,
                parameters: <String>[
                  '${_unownedArgumentType(nonNullType)} value_arg'
                ]);
          }
          indent.newln();
        }
      });

      _writeAccessBlock(indent, _ClassAccess.private, () {
        _writeFunctionDeclaration(indent, 'FromEncodableList',
            returnType: klass.name,
            parameters: <String>['const flutter::EncodableList& list'],
            isStatic: true);
        _writeFunctionDeclaration(indent, 'ToEncodableList',
            returnType: 'flutter::EncodableList', isConst: true);
        for (final Class friend in root.classes) {
          if (friend != klass &&
              friend.fields.any(
                  (NamedType element) => element.type.baseName == klass.name)) {
            indent.writeln('friend class ${friend.name};');
          }
        }
        for (final Api api in root.apis) {
          // TODO(gaaclarke): Find a way to be more precise with our
          // friendships.
          indent.writeln('friend class ${api.name};');
          indent.writeln('friend class ${_getCodecSerializerName(api)};');
        }
        if (testFixtureClass != null) {
          indent.writeln('friend class $testFixtureClass;');
        }

        for (final NamedType field in orderedFields) {
          final HostDatatype hostDatatype = getFieldHostDatatype(
              field, root.classes, root.enums, _baseCppTypeForBuiltinDartType);
          indent.writeln(
              '${_valueType(hostDatatype)} ${_makeInstanceVariableName(field)};');
        }
      });
    }, nestCount: 0);
    indent.newln();
  }

  @override
  void writeFlutterApi(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Api api, {
    required String dartPackageName,
  }) {
    assert(api.location == ApiLocation.flutter);
    if (getCodecClasses(api, root).isNotEmpty) {
      _writeCodec(generatorOptions, root, indent, api);
    }
    const List<String> generatedMessages = <String>[
      ' Generated class from Pigeon that represents Flutter messages that can be called from C++.'
    ];
    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
        generatorComments: generatedMessages);
    indent.write('class ${api.name} ');
    indent.addScoped('{', '};', () {
      _writeAccessBlock(indent, _ClassAccess.public, () {
        _writeFunctionDeclaration(indent, api.name,
            parameters: <String>['flutter::BinaryMessenger* binary_messenger']);
        _writeFunctionDeclaration(indent, 'GetCodec',
            returnType: 'const flutter::StandardMessageCodec&', isStatic: true);
        for (final Method func in api.methods) {
          final HostDatatype returnType = getHostDatatype(func.returnType,
              root.classes, root.enums, _baseCppTypeForBuiltinDartType);
          addDocumentationComments(
              indent, func.documentationComments, _docCommentSpec);

          final Iterable<String> argTypes = func.arguments.map((NamedType arg) {
            final HostDatatype hostType = getFieldHostDatatype(
                arg, root.classes, root.enums, _baseCppTypeForBuiltinDartType);
            return _flutterApiArgumentType(hostType);
          });
          final Iterable<String> argNames =
              indexMap(func.arguments, _getArgumentName);
          final List<String> parameters = <String>[
            ...map2(argTypes, argNames, (String x, String y) => '$x $y'),
            ..._flutterApiCallbackParameters(returnType),
          ];
          _writeFunctionDeclaration(indent, _makeMethodName(func),
              returnType: _voidType, parameters: parameters);
        }
      });
      indent.addScoped(' private:', null, () {
        indent.writeln('flutter::BinaryMessenger* binary_messenger_;');
      });
    }, nestCount: 0);
    indent.newln();
  }

  @override
  void writeHostApi(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Api api, {
    required String dartPackageName,
  }) {
    assert(api.location == ApiLocation.host);
    if (getCodecClasses(api, root).isNotEmpty) {
      _writeCodec(generatorOptions, root, indent, api);
    }
    const List<String> generatedMessages = <String>[
      ' Generated interface from Pigeon that represents a handler of messages from Flutter.'
    ];
    addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
        generatorComments: generatedMessages);
    indent.write('class ${api.name} ');
    indent.addScoped('{', '};', () {
      _writeAccessBlock(indent, _ClassAccess.public, () {
        // Prevent copying/assigning.
        _writeFunctionDeclaration(indent, api.name,
            parameters: <String>['const ${api.name}&'], deleted: true);
        _writeFunctionDeclaration(indent, 'operator=',
            returnType: '${api.name}&',
            parameters: <String>['const ${api.name}&'],
            deleted: true);
        // No-op virtual destructor.
        _writeFunctionDeclaration(indent, '~${api.name}',
            isVirtual: true, inlineNoop: true);
        for (final Method method in api.methods) {
          final HostDatatype returnType = getHostDatatype(method.returnType,
              root.classes, root.enums, _baseCppTypeForBuiltinDartType);
          final String returnTypeName = _hostApiReturnType(returnType);

          final List<String> parameters = <String>[];
          if (method.arguments.isNotEmpty) {
            final Iterable<String> argTypes =
                method.arguments.map((NamedType arg) {
              final HostDatatype hostType = getFieldHostDatatype(arg,
                  root.classes, root.enums, _baseCppTypeForBuiltinDartType);
              return _hostApiArgumentType(hostType);
            });
            final Iterable<String> argNames =
                method.arguments.map((NamedType e) => _makeVariableName(e));
            parameters.addAll(
                map2(argTypes, argNames, (String argType, String argName) {
              return '$argType $argName';
            }));
          }

          addDocumentationComments(
              indent, method.documentationComments, _docCommentSpec);
          final String methodReturn;
          if (method.isAsynchronous) {
            methodReturn = _voidType;
            parameters.add('std::function<void($returnTypeName reply)> result');
          } else {
            methodReturn = returnTypeName;
          }
          _writeFunctionDeclaration(indent, _makeMethodName(method),
              returnType: methodReturn,
              parameters: parameters,
              isVirtual: true,
              isPureVirtual: true);
        }
        indent.newln();
        indent.writeln('$_commentPrefix The codec used by ${api.name}.');
        _writeFunctionDeclaration(indent, 'GetCodec',
            returnType: 'const flutter::StandardMessageCodec&', isStatic: true);
        indent.writeln(
            '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
        _writeFunctionDeclaration(indent, 'SetUp',
            returnType: _voidType,
            isStatic: true,
            parameters: <String>[
              'flutter::BinaryMessenger* binary_messenger',
              '${api.name}* api',
            ]);
        _writeFunctionDeclaration(indent, 'WrapError',
            returnType: 'flutter::EncodableValue',
            isStatic: true,
            parameters: <String>['std::string_view error_message']);
        _writeFunctionDeclaration(indent, 'WrapError',
            returnType: 'flutter::EncodableValue',
            isStatic: true,
            parameters: <String>['const FlutterError& error']);
      });
      _writeAccessBlock(indent, _ClassAccess.protected, () {
        indent.writeln('${api.name}() = default;');
      });
    }, nestCount: 0);
  }

  void _writeClassConstructor(Root root, Indent indent, Class klass,
      Iterable<NamedType> params, String docComment) {
    final List<String> paramStrings = params.map((NamedType param) {
      final HostDatatype hostDatatype = getFieldHostDatatype(
          param, root.classes, root.enums, _baseCppTypeForBuiltinDartType);
      return '${_hostApiArgumentType(hostDatatype)} ${_makeVariableName(param)}';
    }).toList();
    indent.writeln('$_commentPrefix $docComment');
    _writeFunctionDeclaration(indent, klass.name,
        isConstructor: true, parameters: paramStrings);
    indent.newln();
  }

  void _writeCodec(
      CppOptions generatorOptions, Root root, Indent indent, Api api) {
    assert(getCodecClasses(api, root).isNotEmpty);
    final String codeSerializerName = _getCodecSerializerName(api);
    indent
        .write('class $codeSerializerName : public $_defaultCodecSerializer ');
    indent.addScoped('{', '};', () {
      _writeAccessBlock(indent, _ClassAccess.public, () {
        _writeFunctionDeclaration(indent, codeSerializerName,
            isConstructor: true);
        _writeFunctionDeclaration(indent, 'GetInstance',
            returnType: '$codeSerializerName&', isStatic: true, inlineBody: () {
          indent.writeln('static $codeSerializerName sInstance;');
          indent.writeln('return sInstance;');
        });
        indent.newln();
        _writeFunctionDeclaration(indent, 'WriteValue',
            returnType: _voidType,
            parameters: <String>[
              'const flutter::EncodableValue& value',
              'flutter::ByteStreamWriter* stream'
            ],
            isConst: true,
            isOverride: true);
      });
      indent.writeScoped(' protected:', '', () {
        _writeFunctionDeclaration(indent, 'ReadValueOfType',
            returnType: 'flutter::EncodableValue',
            parameters: <String>[
              'uint8_t type',
              'flutter::ByteStreamReader* stream'
            ],
            isConst: true,
            isOverride: true);
      });
    }, nestCount: 0);
    indent.newln();
  }

  void _writeErrorOr(Indent indent,
      {Iterable<String> friends = const <String>[]}) {
    final String friendLines = friends
        .map((String className) => '\tfriend class $className;')
        .join('\n');
    indent.format('''

class FlutterError {
 public:
\texplicit FlutterError(const std::string& code)
\t\t: code_(code) {}
\texplicit FlutterError(const std::string& code, const std::string& message)
\t\t: code_(code), message_(message) {}
\texplicit FlutterError(const std::string& code, const std::string& message, const flutter::EncodableValue& details)
\t\t: code_(code), message_(message), details_(details) {}

\tconst std::string& code() const { return code_; }
\tconst std::string& message() const { return message_; }
\tconst flutter::EncodableValue& details() const { return details_; }

 private:
\tstd::string code_;
\tstd::string message_;
\tflutter::EncodableValue details_;
};

template<class T> class ErrorOr {
 public:
\tErrorOr(const T& rhs) : v_(rhs) {}
\tErrorOr(const T&& rhs) : v_(std::move(rhs)) {}
\tErrorOr(const FlutterError& rhs) : v_(rhs) {}
\tErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {}

\tbool has_error() const { return std::holds_alternative<FlutterError>(v_); }
\tconst T& value() const { return std::get<T>(v_); };
\tconst FlutterError& error() const { return std::get<FlutterError>(v_); };

 private:
$friendLines
\tErrorOr() = default;
\tT TakeValue() && { return std::get<T>(std::move(v_)); }

\tstd::variant<T, FlutterError> v_;
};
''');
  }

  @override
  void writeCloseNamespace(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    if (generatorOptions.namespace != null) {
      indent.writeln('}  // namespace ${generatorOptions.namespace}');
    }
    final String guardName = _getGuardName(generatorOptions.headerIncludePath);
    indent.writeln('#endif  // $guardName');
  }
}

/// Writes C++ source (.cpp) file to sink.
class CppSourceGenerator extends StructuredGenerator<CppOptions> {
  /// Constructor.
  const CppSourceGenerator();

  @override
  void writeFilePrologue(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    if (generatorOptions.copyrightHeader != null) {
      addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
    }
    indent.writeln('$_commentPrefix ${getGeneratedCodeWarning()}');
    indent.writeln('$_commentPrefix $seeAlsoWarning');
    indent.newln();
    indent.addln('#undef _HAS_EXCEPTIONS');
    indent.newln();
  }

  @override
  void writeFileImports(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    indent.writeln('#include "${generatorOptions.headerIncludePath}"');
    indent.newln();
    _writeSystemHeaderIncludeBlock(indent, <String>[
      'flutter/basic_message_channel.h',
      'flutter/binary_messenger.h',
      'flutter/encodable_value.h',
      'flutter/standard_message_codec.h',
    ]);
    indent.newln();
    _writeSystemHeaderIncludeBlock(indent, <String>[
      'map',
      'string',
      'optional',
    ]);
    indent.newln();
  }

  @override
  void writeOpenNamespace(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    if (generatorOptions.namespace != null) {
      indent.writeln('namespace ${generatorOptions.namespace} {');
    }
  }

  @override
  void writeGeneralUtilities(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    final List<String> usingDirectives = <String>[
      'flutter::BasicMessageChannel',
      'flutter::CustomEncodableValue',
      'flutter::EncodableList',
      'flutter::EncodableMap',
      'flutter::EncodableValue',
    ];
    usingDirectives.sort();
    for (final String using in usingDirectives) {
      indent.writeln('using $using;');
    }
    indent.newln();
  }

  @override
  void writeDataClass(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Class klass, {
    required String dartPackageName,
  }) {
    final Set<String> customClassNames =
        root.classes.map((Class x) => x.name).toSet();
    final Set<String> customEnumNames =
        root.enums.map((Enum x) => x.name).toSet();

    indent.writeln('$_commentPrefix ${klass.name}');
    indent.newln();

    final Iterable<NamedType> orderedFields =
        getFieldsInSerializationOrder(klass);
    final Iterable<NamedType> requiredFields =
        orderedFields.where((NamedType type) => !type.type.isNullable);
    // Minimal constructor, if needed.
    if (requiredFields.length != orderedFields.length) {
      _writeClassConstructor(root, indent, klass, requiredFields);
    }
    // All-field constructor.
    _writeClassConstructor(root, indent, klass, orderedFields);

    // Getters and setters.
    for (final NamedType field in orderedFields) {
      _writeCppSourceClassField(generatorOptions, root, indent, klass, field);
    }

    // Serialization.
    writeClassEncode(
      generatorOptions,
      root,
      indent,
      klass,
      customClassNames,
      customEnumNames,
      dartPackageName: dartPackageName,
    );

    // Deserialization.
    writeClassDecode(
      generatorOptions,
      root,
      indent,
      klass,
      customClassNames,
      customEnumNames,
      dartPackageName: dartPackageName,
    );
  }

  @override
  void writeClassEncode(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Class klass,
    Set<String> customClassNames,
    Set<String> customEnumNames, {
    required String dartPackageName,
  }) {
    _writeFunctionDefinition(indent, 'ToEncodableList',
        scope: klass.name,
        returnType: 'EncodableList',
        isConst: true, body: () {
      indent.writeln('EncodableList list;');
      indent.writeln('list.reserve(${klass.fields.length});');
      for (final NamedType field in getFieldsInSerializationOrder(klass)) {
        final HostDatatype hostDatatype = getFieldHostDatatype(field,
            root.classes, root.enums, _shortBaseCppTypeForBuiltinDartType);
        final String encodableValue = _wrappedHostApiArgumentExpression(
            root, _makeInstanceVariableName(field), field.type, hostDatatype,
            preSerializeClasses: true);
        indent.writeln('list.push_back($encodableValue);');
      }
      indent.writeln('return list;');
    });
  }

  @override
  void writeClassDecode(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Class klass,
    Set<String> customClassNames,
    Set<String> customEnumNames, {
    required String dartPackageName,
  }) {
    // Returns the expression to convert the given EncodableValue to a field
    // value.
    String getValueExpression(NamedType field, String encodable) {
      if (customEnumNames.contains(field.type.baseName)) {
        return '(${field.type.baseName})(std::get<int32_t>($encodable))';
      } else if (field.type.baseName == 'int') {
        return '$encodable.LongValue()';
      } else if (field.type.baseName == 'Object') {
        return encodable;
      } else {
        final HostDatatype hostDatatype = getFieldHostDatatype(field,
            root.classes, root.enums, _shortBaseCppTypeForBuiltinDartType);
        if (!hostDatatype.isBuiltin &&
            root.classes
                .map((Class x) => x.name)
                .contains(field.type.baseName)) {
          return '${hostDatatype.datatype}::FromEncodableList(std::get<EncodableList>($encodable))';
        } else {
          return 'std::get<${hostDatatype.datatype}>($encodable)';
        }
      }
    }

    _writeFunctionDefinition(indent, 'FromEncodableList',
        scope: klass.name,
        returnType: klass.name,
        parameters: <String>['const EncodableList& list'], body: () {
      const String instanceVariable = 'decoded';
      final Iterable<_IndexedField> indexedFields = indexMap(
          getFieldsInSerializationOrder(klass),
          (int index, NamedType field) => _IndexedField(index, field));
      final Iterable<_IndexedField> nullableFields = indexedFields
          .where((_IndexedField field) => field.field.type.isNullable);
      final Iterable<_IndexedField> nonNullableFields = indexedFields
          .where((_IndexedField field) => !field.field.type.isNullable);

      // Non-nullable fields must be set via the constructor.
      String constructorArgs = nonNullableFields
          .map((_IndexedField param) =>
              getValueExpression(param.field, 'list[${param.index}]'))
          .join(',\n\t');
      if (constructorArgs.isNotEmpty) {
        constructorArgs = '(\n\t$constructorArgs)';
      }
      indent.format('${klass.name} $instanceVariable$constructorArgs;');

      // Add the nullable fields via setters, since converting the encodable
      // values to the pointer types that the convenience constructor uses for
      // nullable fields is non-trivial.
      for (final _IndexedField entry in nullableFields) {
        final NamedType field = entry.field;
        final String setterName = _makeSetterName(field);
        final String encodableFieldName =
            '${_encodablePrefix}_${_makeVariableName(field)}';
        indent.writeln('auto& $encodableFieldName = list[${entry.index}];');

        final String valueExpression =
            getValueExpression(field, encodableFieldName);
        indent.writeScoped('if (!$encodableFieldName.IsNull()) {', '}', () {
          indent.writeln('$instanceVariable.$setterName($valueExpression);');
        });
      }

      // This returns by value, relying on copy elision, since it makes the
      // usage more convenient during deserialization than it would be with
      // explicit transfer via unique_ptr.
      indent.writeln('return $instanceVariable;');
    });
  }

  @override
  void writeFlutterApi(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Api api, {
    required String dartPackageName,
  }) {
    assert(api.location == ApiLocation.flutter);
    if (getCodecClasses(api, root).isNotEmpty) {
      _writeCodec(generatorOptions, root, indent, api);
    }
    indent.writeln(
        '$_commentPrefix Generated class from Pigeon that represents Flutter messages that can be called from C++.');
    _writeFunctionDefinition(indent, api.name,
        scope: api.name,
        parameters: <String>['flutter::BinaryMessenger* binary_messenger'],
        initializers: <String>['binary_messenger_(binary_messenger)']);
    final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
        ? _getCodecSerializerName(api)
        : _defaultCodecSerializer;
    _writeFunctionDefinition(indent, 'GetCodec',
        scope: api.name,
        returnType: 'const flutter::StandardMessageCodec&', body: () {
      indent.writeln(
          'return flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());');
    });
    for (final Method func in api.methods) {
      final String channelName = makeChannelName(api, func, dartPackageName);
      final HostDatatype returnType = getHostDatatype(func.returnType,
          root.classes, root.enums, _shortBaseCppTypeForBuiltinDartType);

      // Determine the input parameter list, saved in a structured form for later
      // use as platform channel call arguments.
      final Iterable<_HostNamedType> hostParameters =
          indexMap(func.arguments, (int i, NamedType arg) {
        final HostDatatype hostType = getFieldHostDatatype(
            arg, root.classes, root.enums, _shortBaseCppTypeForBuiltinDartType);
        return _HostNamedType(_getSafeArgumentName(i, arg), hostType, arg.type);
      });
      final List<String> parameters = <String>[
        ...hostParameters.map((_HostNamedType arg) =>
            '${_flutterApiArgumentType(arg.hostType)} ${arg.name}'),
        ..._flutterApiCallbackParameters(returnType),
      ];
      _writeFunctionDefinition(indent, _makeMethodName(func),
          scope: api.name,
          returnType: _voidType,
          parameters: parameters, body: () {
        const String channel = 'channel';
        indent.writeln(
            'auto channel = std::make_unique<BasicMessageChannel<>>(binary_messenger_, '
            '"$channelName", &GetCodec());');

        // Convert arguments to EncodableValue versions.
        const String argumentListVariableName = 'encoded_api_arguments';
        indent.write('EncodableValue $argumentListVariableName = ');
        if (func.arguments.isEmpty) {
          indent.addln('EncodableValue();');
        } else {
          indent.addScoped('EncodableValue(EncodableList{', '});', () {
            for (final _HostNamedType param in hostParameters) {
              final String encodedArgument = _wrappedHostApiArgumentExpression(
                  root, param.name, param.originalType, param.hostType,
                  preSerializeClasses: false);
              indent.writeln('$encodedArgument,');
            }
          });
        }

        indent.write('$channel->Send($argumentListVariableName, '
            // ignore: missing_whitespace_between_adjacent_strings
            '[on_success = std::move(on_success), on_error = std::move(on_error)]'
            '(const uint8_t* reply, size_t reply_size) ');
        indent.addScoped('{', '});', () {
          final String successCallbackArgument;
          if (func.returnType.isVoid) {
            successCallbackArgument = '';
          } else {
            successCallbackArgument = 'return_value';
            final String encodedReplyName =
                'encodable_$successCallbackArgument';
            indent.writeln(
                'std::unique_ptr<EncodableValue> response = GetCodec().DecodeMessage(reply, reply_size);');
            indent.writeln('const auto& $encodedReplyName = *response;');
            _writeEncodableValueArgumentUnwrapping(
              indent,
              root,
              returnType,
              argName: successCallbackArgument,
              encodableArgName: encodedReplyName,
              apiType: ApiType.flutter,
            );
          }
          indent.writeln('on_success($successCallbackArgument);');
        });
      });
    }
  }

  @override
  void writeHostApi(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Api api, {
    required String dartPackageName,
  }) {
    assert(api.location == ApiLocation.host);
    if (getCodecClasses(api, root).isNotEmpty) {
      _writeCodec(generatorOptions, root, indent, api);
    }

    final String codeSerializerName = getCodecClasses(api, root).isNotEmpty
        ? _getCodecSerializerName(api)
        : _defaultCodecSerializer;
    indent.writeln('/// The codec used by ${api.name}.');
    _writeFunctionDefinition(indent, 'GetCodec',
        scope: api.name,
        returnType: 'const flutter::StandardMessageCodec&', body: () {
      indent.writeln(
          'return flutter::StandardMessageCodec::GetInstance(&$codeSerializerName::GetInstance());');
    });
    indent.writeln(
        '$_commentPrefix Sets up an instance of `${api.name}` to handle messages through the `binary_messenger`.');
    _writeFunctionDefinition(indent, 'SetUp',
        scope: api.name,
        returnType: _voidType,
        parameters: <String>[
          'flutter::BinaryMessenger* binary_messenger',
          '${api.name}* api'
        ], body: () {
      for (final Method method in api.methods) {
        final String channelName =
            makeChannelName(api, method, dartPackageName);
        indent.writeScoped('{', '}', () {
          indent.writeln(
              'auto channel = std::make_unique<BasicMessageChannel<>>(binary_messenger, '
              '"$channelName", &GetCodec());');
          indent.writeScoped('if (api != nullptr) {', '} else {', () {
            indent.write(
                'channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply<EncodableValue>& reply) ');
            indent.addScoped('{', '});', () {
              indent.writeScoped('try {', '}', () {
                final List<String> methodArgument = <String>[];
                if (method.arguments.isNotEmpty) {
                  indent.writeln(
                      'const auto& args = std::get<EncodableList>(message);');

                  enumerate(method.arguments, (int index, NamedType arg) {
                    final HostDatatype hostType = getHostDatatype(
                        arg.type,
                        root.classes,
                        root.enums,
                        (TypeDeclaration x) =>
                            _shortBaseCppTypeForBuiltinDartType(x));
                    final String argName = _getSafeArgumentName(index, arg);

                    final String encodableArgName =
                        '${_encodablePrefix}_$argName';
                    indent.writeln(
                        'const auto& $encodableArgName = args.at($index);');
                    if (!arg.type.isNullable) {
                      indent.writeScoped(
                          'if ($encodableArgName.IsNull()) {', '}', () {
                        indent.writeln(
                            'reply(WrapError("$argName unexpectedly null."));');
                        indent.writeln('return;');
                      });
                    }
                    _writeEncodableValueArgumentUnwrapping(
                      indent,
                      root,
                      hostType,
                      argName: argName,
                      encodableArgName: encodableArgName,
                      apiType: ApiType.host,
                    );
                    final String unwrapEnum =
                        isEnum(root, arg.type) && arg.type.isNullable
                            ? ' ? &(*$argName) : nullptr'
                            : '';
                    methodArgument.add('$argName$unwrapEnum');
                  });
                }

                final HostDatatype returnType = getHostDatatype(
                    method.returnType,
                    root.classes,
                    root.enums,
                    _shortBaseCppTypeForBuiltinDartType);
                final String returnTypeName = _hostApiReturnType(returnType);
                if (method.isAsynchronous) {
                  methodArgument.add(
                    '[reply]($returnTypeName&& output) {${indent.newline}'
                    '${_wrapResponse(indent, root, method.returnType, prefix: '\t')}${indent.newline}'
                    '}',
                  );
                }
                final String call =
                    'api->${_makeMethodName(method)}(${methodArgument.join(', ')})';
                if (method.isAsynchronous) {
                  indent.format('$call;');
                } else {
                  indent.writeln('$returnTypeName output = $call;');
                  indent.format(_wrapResponse(indent, root, method.returnType));
                }
              }, addTrailingNewline: false);
              indent.add(' catch (const std::exception& exception) ');
              indent.addScoped('{', '}', () {
                // There is a potential here for `reply` to be called twice, which
                // is a violation of the API contract, because there's no way of
                // knowing whether or not the plugin code called `reply` before
                // throwing. Since use of `@async` suggests that the reply is
                // probably not sent within the scope of the stack, err on the
                // side of potential double-call rather than no call (which is
                // also an API violation) so that unexpected errors have a better
                // chance of being caught and handled in a useful way.
                indent.writeln('reply(WrapError(exception.what()));');
              });
            });
          });
          indent.addScoped(null, '}', () {
            indent.writeln('channel->SetMessageHandler(nullptr);');
          });
        });
      }
    });

    _writeFunctionDefinition(indent, 'WrapError',
        scope: api.name,
        returnType: 'EncodableValue',
        parameters: <String>['std::string_view error_message'], body: () {
      indent.format('''
return EncodableValue(EncodableList{
\tEncodableValue(std::string(error_message)),
\tEncodableValue("Error"),
\tEncodableValue()
});''');
    });
    _writeFunctionDefinition(indent, 'WrapError',
        scope: api.name,
        returnType: 'EncodableValue',
        parameters: <String>['const FlutterError& error'], body: () {
      indent.format('''
return EncodableValue(EncodableList{
\tEncodableValue(error.code()),
\tEncodableValue(error.message()),
\terror.details()
});''');
    });
  }

  void _writeCodec(
    CppOptions generatorOptions,
    Root root,
    Indent indent,
    Api api,
  ) {
    assert(getCodecClasses(api, root).isNotEmpty);
    final String codeSerializerName = _getCodecSerializerName(api);
    indent.newln();
    _writeFunctionDefinition(indent, codeSerializerName,
        scope: codeSerializerName);
    _writeFunctionDefinition(indent, 'ReadValueOfType',
        scope: codeSerializerName,
        returnType: 'EncodableValue',
        parameters: <String>[
          'uint8_t type',
          'flutter::ByteStreamReader* stream',
        ],
        isConst: true, body: () {
      indent.write('switch (type) ');
      indent.addScoped('{', '}', () {
        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
          indent.writeln('case ${customClass.enumeration}:');
          indent.nest(1, () {
            indent.writeln(
                'return CustomEncodableValue(${customClass.name}::FromEncodableList(std::get<EncodableList>(ReadValue(stream))));');
          });
        }
        indent.writeln('default:');
        indent.nest(1, () {
          indent.writeln(
              'return $_defaultCodecSerializer::ReadValueOfType(type, stream);');
        });
      });
    });
    _writeFunctionDefinition(indent, 'WriteValue',
        scope: codeSerializerName,
        returnType: _voidType,
        parameters: <String>[
          'const EncodableValue& value',
          'flutter::ByteStreamWriter* stream',
        ],
        isConst: true, body: () {
      indent.write(
          'if (const CustomEncodableValue* custom_value = std::get_if<CustomEncodableValue>(&value)) ');
      indent.addScoped('{', '}', () {
        for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
          indent.write(
              'if (custom_value->type() == typeid(${customClass.name})) ');
          indent.addScoped('{', '}', () {
            indent.writeln('stream->WriteByte(${customClass.enumeration});');
            indent.writeln(
                'WriteValue(EncodableValue(std::any_cast<${customClass.name}>(*custom_value).ToEncodableList()), stream);');
            indent.writeln('return;');
          });
        }
      });
      indent.writeln('$_defaultCodecSerializer::WriteValue(value, stream);');
    });
  }

  void _writeClassConstructor(
      Root root, Indent indent, Class klass, Iterable<NamedType> params) {
    final Iterable<_HostNamedType> hostParams = params.map((NamedType param) {
      return _HostNamedType(
        _makeVariableName(param),
        getFieldHostDatatype(
          param,
          root.classes,
          root.enums,
          _shortBaseCppTypeForBuiltinDartType,
        ),
        param.type,
      );
    });

    final List<String> paramStrings = hostParams
        .map((_HostNamedType param) =>
            '${_hostApiArgumentType(param.hostType)} ${param.name}')
        .toList();
    final List<String> initializerStrings = hostParams
        .map((_HostNamedType param) =>
            '${param.name}_(${_fieldValueExpression(param.hostType, param.name)})')
        .toList();
    _writeFunctionDefinition(indent, klass.name,
        scope: klass.name,
        parameters: paramStrings,
        initializers: initializerStrings);
  }

  void _writeCppSourceClassField(CppOptions generatorOptions, Root root,
      Indent indent, Class klass, NamedType field) {
    final HostDatatype hostDatatype = getFieldHostDatatype(
        field, root.classes, root.enums, _shortBaseCppTypeForBuiltinDartType);
    final String instanceVariableName = _makeInstanceVariableName(field);
    final String setterName = _makeSetterName(field);
    final String returnExpression = hostDatatype.isNullable
        ? '$instanceVariableName ? &(*$instanceVariableName) : nullptr'
        : instanceVariableName;

    // Writes a setter treating the type as [type], to allow generating multiple
    // setter variants.
    void writeSetter(HostDatatype type) {
      const String setterArgumentName = 'value_arg';
      _writeFunctionDefinition(
        indent,
        setterName,
        scope: klass.name,
        returnType: _voidType,
        parameters: <String>[
          '${_unownedArgumentType(type)} $setterArgumentName'
        ],
        body: () {
          indent.writeln(
              '$instanceVariableName = ${_fieldValueExpression(type, setterArgumentName)};');
        },
      );
    }

    _writeFunctionDefinition(
      indent,
      _makeGetterName(field),
      scope: klass.name,
      returnType: _getterReturnType(hostDatatype),
      isConst: true,
      body: () {
        indent.writeln('return $returnExpression;');
      },
    );
    writeSetter(hostDatatype);
    if (hostDatatype.isNullable) {
      // Write the non-nullable variant; see _writeCppHeaderDataClass.
      writeSetter(_nonNullableType(hostDatatype));
    }

    indent.newln();
  }

  /// Returns the value to use when setting a field of the given type from
  /// an argument of that type.
  ///
  /// For non-nullable values this is just the variable itself, but for nullable
  /// values this handles the conversion between an argument type (a pointer)
  /// and the field type (a std::optional).
  String _fieldValueExpression(HostDatatype type, String variable) {
    return type.isNullable
        ? '$variable ? ${_valueType(type)}(*$variable) : std::nullopt'
        : variable;
  }

  String _wrapResponse(Indent indent, Root root, TypeDeclaration returnType,
      {String prefix = ''}) {
    final String nonErrorPath;
    final String errorCondition;
    final String errorGetter;

    const String nullValue = 'EncodableValue()';
    String enumPrefix = '';
    if (isEnum(root, returnType)) {
      enumPrefix = '(int) ';
    }
    if (returnType.isVoid) {
      nonErrorPath = '${prefix}wrapped.push_back($nullValue);';
      errorCondition = 'output.has_value()';
      errorGetter = 'value';
    } else {
      final HostDatatype hostType = getHostDatatype(returnType, root.classes,
          root.enums, _shortBaseCppTypeForBuiltinDartType);

      const String extractedValue = 'std::move(output).TakeValue()';
      final String wrapperType = hostType.isBuiltin || isEnum(root, returnType)
          ? 'EncodableValue'
          : 'CustomEncodableValue';
      if (returnType.isNullable) {
        // The value is a std::optional, so needs an extra layer of
        // handling.
        nonErrorPath = '''
${prefix}auto output_optional = $extractedValue;
${prefix}if (output_optional) {
$prefix\twrapped.push_back($wrapperType(${enumPrefix}std::move(output_optional).value()));
$prefix} else {
$prefix\twrapped.push_back($nullValue);
$prefix}''';
      } else {
        nonErrorPath =
            '${prefix}wrapped.push_back($wrapperType($enumPrefix$extractedValue));';
      }
      errorCondition = 'output.has_error()';
      errorGetter = 'error';
    }
    // Ideally this code would use an initializer list to create
    // an EncodableList inline, which would be less code. However,
    // that would always copy the element, so the slightly more
    // verbose create-and-push approach is used instead.
    return '''
${prefix}if ($errorCondition) {
$prefix\treply(WrapError(output.$errorGetter()));
$prefix\treturn;
$prefix}
${prefix}EncodableList wrapped;
$nonErrorPath
${prefix}reply(EncodableValue(std::move(wrapped)));''';
  }

  @override
  void writeCloseNamespace(
    CppOptions generatorOptions,
    Root root,
    Indent indent, {
    required String dartPackageName,
  }) {
    if (generatorOptions.namespace != null) {
      indent.writeln('}  // namespace ${generatorOptions.namespace}');
    }
  }

  /// Returns the expression to create an EncodableValue from a host API argument
  /// with the given [variableName] and types.
  ///
  /// If [preSerializeClasses] is true, custom classes will be returned as
  /// encodable lists rather than CustomEncodableValues; see
  /// https://github.com/flutter/flutter/issues/119351 for why this is currently
  /// needed.
  String _wrappedHostApiArgumentExpression(Root root, String variableName,
      TypeDeclaration dartType, HostDatatype hostType,
      {required bool preSerializeClasses}) {
    final String encodableValue;
    if (!hostType.isBuiltin &&
        root.classes.any((Class c) => c.name == dartType.baseName)) {
      if (preSerializeClasses) {
        final String operator = hostType.isNullable ? '->' : '.';
        encodableValue =
            'EncodableValue($variableName${operator}ToEncodableList())';
      } else {
        final String nonNullValue =
            hostType.isNullable ? '*$variableName' : variableName;
        encodableValue = 'CustomEncodableValue($nonNullValue)';
      }
    } else if (!hostType.isBuiltin &&
        root.enums.any((Enum e) => e.name == dartType.baseName)) {
      final String nonNullValue =
          hostType.isNullable ? '(*$variableName)' : variableName;
      encodableValue = 'EncodableValue((int)$nonNullValue)';
    } else if (dartType.baseName == 'Object') {
      final String operator = hostType.isNullable ? '*' : '';
      encodableValue = '$operator$variableName';
    } else {
      final String operator = hostType.isNullable ? '*' : '';
      encodableValue = 'EncodableValue($operator$variableName)';
    }

    if (hostType.isNullable) {
      return '$variableName ? $encodableValue : EncodableValue()';
    }
    return encodableValue;
  }

  /// Writes the code to declare and populate a variable of type [hostType]
  /// called [argName] to use as a parameter to an API method call, from an
  /// existing EncodableValue variable called [encodableArgName].
  void _writeEncodableValueArgumentUnwrapping(
    Indent indent,
    Root root,
    HostDatatype hostType, {
    required String argName,
    required String encodableArgName,
    required ApiType apiType,
  }) {
    if (hostType.isNullable) {
      // Nullable arguments are always pointers, with nullptr corresponding to
      // null.
      if (hostType.datatype == 'int64_t') {
        // The EncodableValue will either be an int32_t or an int64_t depending
        // on the value, but the generated API requires an int64_t so that it can
        // handle any case. Create a local variable for the 64-bit value...
        final String valueVarName = '${argName}_value';
        indent.writeln(
            'const int64_t $valueVarName = $encodableArgName.IsNull() ? 0 : $encodableArgName.LongValue();');
        // ... then declare the arg as a reference to that local.
        indent.writeln(
            'const auto* $argName = $encodableArgName.IsNull() ? nullptr : &$valueVarName;');
      } else if (hostType.datatype == 'EncodableValue') {
        // Generic objects just pass the EncodableValue through directly.
        indent.writeln('const auto* $argName = &$encodableArgName;');
      } else if (hostType.isBuiltin) {
        indent.writeln(
            'const auto* $argName = std::get_if<${hostType.datatype}>(&$encodableArgName);');
      } else if (hostType.isEnum) {
        if (hostType.isNullable) {
          final String valueVarName = '${argName}_value';
          indent.writeln(
              'const int64_t $valueVarName = $encodableArgName.IsNull() ? 0 : $encodableArgName.LongValue();');
          if (apiType == ApiType.flutter) {
            indent.writeln(
                'const auto* $argName = $encodableArgName.IsNull() ? nullptr : &(${hostType.datatype})$valueVarName;');
          } else {
            indent.writeln(
                'const auto $argName = $encodableArgName.IsNull() ? std::nullopt : std::make_optional<${hostType.datatype}>(static_cast<${hostType.datatype}>(${argName}_value));');
          }
        } else {
          indent.writeln(
              'const auto* $argName = &((${hostType.datatype})std::get<int>($encodableArgName));');
        }
      } else {
        indent.writeln(
            'const auto* $argName = &(std::any_cast<const ${hostType.datatype}&>(std::get<CustomEncodableValue>($encodableArgName)));');
      }
    } else {
      // Non-nullable arguments are either passed by value or reference, but the
      // extraction doesn't need to distinguish since those are the same at the
      // call site.
      if (hostType.datatype == 'int64_t') {
        // The EncodableValue will either be an int32_t or an int64_t depending
        // on the value, but the generated API requires an int64_t so that it can
        // handle any case.
        indent
            .writeln('const int64_t $argName = $encodableArgName.LongValue();');
      } else if (hostType.datatype == 'EncodableValue') {
        // Generic objects just pass the EncodableValue through directly. This
        // creates an alias just to avoid having to special-case the
        // argName/encodableArgName distinction at a higher level.
        indent.writeln('const auto& $argName = $encodableArgName;');
      } else if (hostType.isBuiltin) {
        indent.writeln(
            'const auto& $argName = std::get<${hostType.datatype}>($encodableArgName);');
      } else if (hostType.isEnum) {
        indent.writeln(
            'const ${hostType.datatype}& $argName = (${hostType.datatype})$encodableArgName.LongValue();');
      } else {
        indent.writeln(
            'const auto& $argName = std::any_cast<const ${hostType.datatype}&>(std::get<CustomEncodableValue>($encodableArgName));');
      }
    }
  }

  /// A wrapper for [_baseCppTypeForBuiltinDartType] that generated Flutter
  /// types without the namespace, since the implementation file uses `using`
  /// directives.
  String? _shortBaseCppTypeForBuiltinDartType(TypeDeclaration type) {
    return _baseCppTypeForBuiltinDartType(type, includeFlutterNamespace: false);
  }
}

/// Contains information about a host function argument.
///
/// This is comparable to a [NamedType], but has already gone through host type
/// and variable name mapping, and it tracks the original [NamedType] that it
/// was created from.
class _HostNamedType {
  const _HostNamedType(this.name, this.hostType, this.originalType);
  final String name;
  final HostDatatype hostType;
  final TypeDeclaration originalType;
}

/// Contains a class field and its serialization index.
class _IndexedField {
  const _IndexedField(this.index, this.field);
  final int index;
  final NamedType field;
}

String _getCodecSerializerName(Api api) => '${api.name}CodecSerializer';

const String _encodablePrefix = 'encodable';

String _getArgumentName(int count, NamedType argument) =>
    argument.name.isEmpty ? 'arg$count' : _makeVariableName(argument);

/// Returns an argument name that can be used in a context where it is possible to collide.
String _getSafeArgumentName(int count, NamedType argument) =>
    '${_getArgumentName(count, argument)}_arg';

/// Returns a non-nullable variant of [type].
HostDatatype _nonNullableType(HostDatatype type) {
  return HostDatatype(
    datatype: type.datatype,
    isBuiltin: type.isBuiltin,
    isNullable: false,
    isEnum: type.isEnum,
  );
}

String _pascalCaseFromCamelCase(String camelCase) =>
    camelCase[0].toUpperCase() + camelCase.substring(1);

String _snakeCaseFromCamelCase(String camelCase) {
  return camelCase.replaceAllMapped(RegExp(r'[A-Z]'),
      (Match m) => '${m.start == 0 ? '' : '_'}${m[0]!.toLowerCase()}');
}

String _pascalCaseFromSnakeCase(String snakeCase) {
  final String camelCase = snakeCase.replaceAllMapped(
      RegExp(r'_([a-z])'), (Match m) => m[1]!.toUpperCase());
  return _pascalCaseFromCamelCase(camelCase);
}

String _makeMethodName(Method method) => _pascalCaseFromCamelCase(method.name);

String _makeGetterName(NamedType field) => _snakeCaseFromCamelCase(field.name);

String _makeSetterName(NamedType field) =>
    'set_${_snakeCaseFromCamelCase(field.name)}';

String _makeVariableName(NamedType field) =>
    _snakeCaseFromCamelCase(field.name);

String _makeInstanceVariableName(NamedType field) =>
    '${_makeVariableName(field)}_';

// TODO(stuartmorgan): Remove this in favor of _isPodType once callers have
// all been updated to using HostDatatypes.
bool _isReferenceType(String dataType) {
  switch (dataType) {
    case 'bool':
    case 'int64_t':
    case 'double':
      return false;
    default:
      return true;
  }
}

/// Returns the parameters to use for the success and error callbacks in a
/// Flutter API function signature.
List<String> _flutterApiCallbackParameters(HostDatatype returnType) {
  return <String>[
    'std::function<void(${_flutterApiReturnType(returnType)})>&& on_success',
    'std::function<void(const FlutterError&)>&& on_error',
  ];
}

/// Returns true if [type] corresponds to a plain-old-data type (i.e., one that
/// should generally be passed by value rather than pointer/reference) in C++.
bool _isPodType(HostDatatype type) {
  return !_isReferenceType(type.datatype);
}

String? _baseCppTypeForBuiltinDartType(
  TypeDeclaration type, {
  bool includeFlutterNamespace = true,
}) {
  final String flutterNamespace = includeFlutterNamespace ? 'flutter::' : '';
  final Map<String, String> cppTypeForDartTypeMap = <String, String>{
    'void': 'void',
    'bool': 'bool',
    'int': 'int64_t',
    'String': 'std::string',
    'double': 'double',
    'Uint8List': 'std::vector<uint8_t>',
    'Int32List': 'std::vector<int32_t>',
    'Int64List': 'std::vector<int64_t>',
    'Float64List': 'std::vector<double>',
    'Map': '${flutterNamespace}EncodableMap',
    'List': '${flutterNamespace}EncodableList',
    'Object': '${flutterNamespace}EncodableValue',
  };
  if (cppTypeForDartTypeMap.containsKey(type.baseName)) {
    return cppTypeForDartTypeMap[type.baseName];
  } else {
    return null;
  }
}

/// Returns the C++ type to use in a value context (variable declaration,
/// pass-by-value, etc.) for the given C++ base type.
String _valueType(HostDatatype type) {
  final String baseType = type.datatype;
  return type.isNullable ? 'std::optional<$baseType>' : baseType;
}

/// Returns the C++ type to use in an argument context without ownership
/// transfer for the given base type.
String _unownedArgumentType(HostDatatype type) {
  final bool isString = type.datatype == 'std::string';
  final String baseType = isString ? 'std::string_view' : type.datatype;
  if (isString || _isPodType(type)) {
    return type.isNullable ? 'const $baseType*' : baseType;
  }
  // TODO(stuartmorgan): Consider special-casing `Object?` here, so that there
  // aren't two ways of representing null (nullptr or an isNull EncodableValue).
  return type.isNullable ? 'const $baseType*' : 'const $baseType&';
}

/// Returns the C++ type to use for arguments to a host API. This is slightly
/// different from [_unownedArgumentType] since passing `std::string_view*` in
/// to the host API implementation when the actual type is `std::string*` is
/// needlessly complicated, so it uses `std::string` directly.
String _hostApiArgumentType(HostDatatype type) {
  final String baseType = type.datatype;
  if (_isPodType(type)) {
    return type.isNullable ? 'const $baseType*' : baseType;
  }
  return type.isNullable ? 'const $baseType*' : 'const $baseType&';
}

/// Returns the C++ type to use for arguments to a Flutter API.
String _flutterApiArgumentType(HostDatatype type) {
  // Nullable strings use std::string* rather than std::string_view*
  // since there's no implicit conversion for the pointer types, making them
  // more awkward to use. For consistency, and since EncodableValue will end
  // up making a std::string internally anyway, std::string is used for the
  // non-nullable case as well.
  if (type.datatype == 'std::string') {
    return type.isNullable ? 'const std::string*' : 'const std::string&';
  }
  return _unownedArgumentType(type);
}

/// Returns the C++ type to use for the return of a getter for a field of type
/// [type].
String _getterReturnType(HostDatatype type) {
  final String baseType = type.datatype;
  if (_isPodType(type)) {
    // Use pointers rather than optionals even for nullable POD, since the
    // semantics of using them is essentially identical and this makes them
    // consistent with non-POD.
    return type.isNullable ? 'const $baseType*' : baseType;
  }
  return type.isNullable ? 'const $baseType*' : 'const $baseType&';
}

/// Returns the C++ type to use for the return of a host API method returning
/// [type].
String _hostApiReturnType(HostDatatype type) {
  if (type.datatype == 'void') {
    return 'std::optional<FlutterError>';
  }
  String valueType = type.datatype;
  if (type.isNullable) {
    valueType = 'std::optional<$valueType>';
  }
  return 'ErrorOr<$valueType>';
}

/// Returns the C++ type to use for the paramer to the asynchronous "return"
/// callback of a Flutter API method returning [type].
String _flutterApiReturnType(HostDatatype type) {
  if (type.datatype == 'void') {
    return 'void';
  }
  // For anything other than void, handle it the same way as a host API argument
  // since it has the same basic structure of being a function defined by the
  // client, being called by the generated code.
  return _hostApiArgumentType(type);
}

String _getGuardName(String? headerFileName) {
  const String prefix = 'PIGEON_';
  if (headerFileName != null) {
    return '$prefix${headerFileName.replaceAll('.', '_').toUpperCase()}_';
  } else {
    return '${prefix}H_';
  }
}

void _writeSystemHeaderIncludeBlock(Indent indent, List<String> headers) {
  headers.sort();
  for (final String header in headers) {
    indent.writeln('#include <$header>');
  }
}

enum _FunctionOutputType { declaration, definition }

/// Writes a function declaration or definition to [indent].
///
/// If [parameters] are given, each should be a string of the form 'type name'.
void _writeFunction(
  Indent indent,
  _FunctionOutputType type, {
  required String name,
  String? returnType,
  String? scope,
  List<String> parameters = const <String>[],
  List<String> startingAnnotations = const <String>[],
  List<String> trailingAnnotations = const <String>[],
  List<String> initializers = const <String>[],
  void Function()? body,
}) {
  assert(body == null || type == _FunctionOutputType.definition);

  // Set the initial indentation.
  indent.write('');
  // Write any starting annotations (e.g., 'static').
  for (final String annotation in startingAnnotations) {
    indent.add('$annotation ');
  }
  // Write the signature.
  if (returnType != null) {
    indent.add('$returnType ');
  }
  if (scope != null) {
    indent.add('$scope::');
  }
  indent.add(name);
  // Write the parameters.
  if (parameters.isEmpty) {
    indent.add('()');
  } else if (parameters.length == 1) {
    indent.add('(${parameters.first})');
  } else {
    indent.addScoped('(', null, () {
      enumerate(parameters, (int index, final String param) {
        if (index == parameters.length - 1) {
          indent.write('$param)');
        } else {
          indent.writeln('$param,');
        }
      });
    }, addTrailingNewline: false);
  }
  // Write any trailing annotations (e.g., 'const').
  for (final String annotation in trailingAnnotations) {
    indent.add(' $annotation');
  }
  // Write the initializer list, if any.
  if (initializers.isNotEmpty) {
    indent.newln();
    indent.write(' : ');
    // The first item goes on the same line as the ":", the rest go on their
    // own lines indented two extra levels, with no comma or newline after the
    // last one. The easiest way to express the special casing of the first and
    // last is with a join+format.
    indent.format(initializers.join(',\n\t\t'),
        leadingSpace: false, trailingNewline: false);
  }
  // Write the body or end the declaration.
  if (type == _FunctionOutputType.declaration) {
    indent.addln(';');
  } else {
    if (body != null) {
      indent.addScoped(' {', '}', body);
    } else {
      indent.addln(' {}');
    }
  }
}

void _writeFunctionDeclaration(
  Indent indent,
  String name, {
  String? returnType,
  List<String> parameters = const <String>[],
  bool isStatic = false,
  bool isVirtual = false,
  bool isConstructor = false,
  bool isPureVirtual = false,
  bool isConst = false,
  bool isOverride = false,
  bool deleted = false,
  bool inlineNoop = false,
  void Function()? inlineBody,
}) {
  assert(!(isVirtual && isOverride), 'virtual is redundant with override');
  assert(isVirtual || !isPureVirtual, 'pure virtual methods must be virtual');
  assert(returnType == null || !isConstructor,
      'constructors cannot have return types');
  _writeFunction(
    indent,
    inlineNoop || (inlineBody != null)
        ? _FunctionOutputType.definition
        : _FunctionOutputType.declaration,
    name: name,
    returnType: returnType,
    parameters: parameters,
    startingAnnotations: <String>[
      if (inlineBody != null) 'inline',
      if (isStatic) 'static',
      if (isVirtual) 'virtual',
      if (isConstructor && parameters.isNotEmpty) 'explicit'
    ],
    trailingAnnotations: <String>[
      if (isConst) 'const',
      if (isOverride) 'override',
      if (deleted) '= delete',
      if (isPureVirtual) '= 0',
    ],
    body: inlineBody,
  );
}

void _writeFunctionDefinition(
  Indent indent,
  String name, {
  String? returnType,
  String? scope,
  List<String> parameters = const <String>[],
  bool isConst = false,
  List<String> initializers = const <String>[],
  void Function()? body,
}) {
  _writeFunction(
    indent,
    _FunctionOutputType.definition,
    name: name,
    scope: scope,
    returnType: returnType,
    parameters: parameters,
    trailingAnnotations: <String>[
      if (isConst) 'const',
    ],
    initializers: initializers,
    body: body,
  );
  indent.newln();
}

enum _ClassAccess { public, protected, private }

void _writeAccessBlock(
    Indent indent, _ClassAccess access, void Function() body) {
  final String accessLabel;
  switch (access) {
    case _ClassAccess.public:
      accessLabel = 'public';
      break;
    case _ClassAccess.protected:
      accessLabel = 'protected';
      break;
    case _ClassAccess.private:
      accessLabel = 'private';
      break;
  }
  indent.addScoped(' $accessLabel:', '', body);
}

/// Validates an AST to make sure the cpp generator supports everything.
List<Error> validateCpp(CppOptions options, Root root) {
  final List<Error> result = <Error>[];
  for (final Api api in root.apis) {
    for (final Method method in api.methods) {
      for (final NamedType arg in method.arguments) {
        if (isEnum(root, arg.type)) {
          // TODO(gaaclarke): Add line number and filename.
          result.add(Error(
              message:
                  "Nullable enum types aren't supported in C++ arguments in method:${api.name}.${method.name} argument:(${arg.type.baseName} ${arg.name})."));
        }
      }
    }
  }
  return result;
}
